package pl.edu.mimuw.mas.visualization.rendering;

import pl.edu.mimuw.mas.bezier.ControlPoints;
import pl.edu.mimuw.mas.chart.Area;
import pl.edu.mimuw.mas.chart.Chart;

/**
 * Kontekst renderowania obiektów {@link Chart} używany przez
 * {@link ChartRenderer}, umożliwia zmianę parametrów renderowania podczas
 * działania aplikacji.
 */
public class ChartRendererContext {
	
	public static final int X0 = 0, Y0 = 1, X1 = 2, Y1 = 3;
	
	public static enum contextType { EDITOR, VISUALIZATION };
	
	// typ kontekstu
	private final contextType type; 
	
	// parametry rzutowania
	private final float frustrumNear = 5.0f;
	private final float frustrumFar = 120.0f;
	private final float frustrumAngle = 30.0f;

	// domyślny widok na mapę
	private final float[] defaultViewAngle = { 90.0f, 0.0f };
	private final float[] defaultChartPosition = { 0.0f, 0.0f,	-(frustrumFar - frustrumNear) / 2 - frustrumNear };
	
	// parametry oświetlenia
	private float[] lightPosition = { 0.0f, 0.0f, 0.0f, 0.0f };
	private float[] lightAmbient = { 0.6f, 0.6f, 0.6f };
		
	// renderowany fragment siatki 
	public final Chart chart;
	public final int[] viewArea;
	
	// początkowe ekstrema
	private float min;
	private float max;
	
	// punkty konrolne
	private ControlPoints controlPoints;

	// kolory - TANGO
	private float[] bgColor = { 0.1796875f, 0.203125f, 0.2109375f, 1.0f };               // {46, 52, 54}
	private float[] activeGridColor = { 0.5390625f, 0.8828125f, 0.203125f };             // {138, 226, 52}
	private float[] inactiveGridColor = { 0.5f, 0.5f, 0.5f };                            // {128, 128, 128}
	private float[] activeControlPointColor = { 0.93359375f, 0.16015625f, 0.16015625f }; // {239, 41, 41}
	private float[] inactiveControlPointColor = { 0.984375f, 0.91015625f, 0.30859375f }; // {252, 233, 79}  
	
	// widok na mapę	
	private int viewMargin = 5;		
	private float[] viewAngle = new float[2];
	
	// domyślny zoom - tyle powinien zajmować procentowo obiekt na płótnie
	private float viewZoom = 0.75f; 
	
	// powiększenie 	
	private float scale;
	private float scaleOffsetRatio = 0.035f;
	
	// wielkość obiektów
	private float nodeRatio = 0.3f;
	private float nodeThreshold = 0.8f;
	
	// zaznaczanie
	private int[] selection = new int[4];
	private boolean showSelected = true;

	// pozycja obiektów
	private float[] chartPosition = new float[4];
	private float moveOffsetRatio = 0.5f;
	
	/**
	 * Widok całej siatki.
	 */
	public ChartRendererContext(Chart chart, contextType type) {
		if (chart == null) {
			throw new NullPointerException();
		}
		this.chart = chart;
		this.viewArea = new int[4];
		this.viewArea[X0] = 0;
		this.viewArea[Y0] = 0;
		this.viewArea[X1] = chart.getColumnsCount() - 1;
		this.viewArea[Y1] = chart.getRowsCount() - 1;

		this.type = type;
		
		restoreDefaultView();
	}

	/**
	 * Widok części siatki.
	 */
	public ChartRendererContext(Chart chart, Area area, contextType type) {
		if (chart == null) {
			throw new NullPointerException();
		}
		this.chart = chart;
		this.viewArea = area.getArea();
		
		this.type = type;
		
		restoreDefaultView();
	}

	/**
	 * Ustawia domyślne wartości widoku.
	 */
	public void restoreDefaultView() {
		int i = 0;
		for (float v : defaultViewAngle) {
			viewAngle[i++] = v;
		}
		i = 0;
		for (float v : defaultChartPosition) {
			chartPosition[i++] = v;
		}
		float sh = autoscale((viewArea[X1] - viewArea[X0]) * chart.getDistance(), -chartPosition[X1], viewZoom, frustrumAngle);
		float sv = autoscale((viewArea[Y1] - viewArea[Y0]) * chart.getDistance(), -chartPosition[X1], viewZoom, frustrumAngle);
		scale = Math.min(sh, sv);
		
		switch (type) {
		case VISUALIZATION:
			minmax();
			break;
		}
	}

	/**
	 * Oblicza minimalne i maksymalne wysokości na mapie.
	 */
	private void minmax() {
		for (int row = viewArea[X0]; row <= viewArea[X1]; row++) {
			for (int col = viewArea[Y0]; col <= viewArea[Y1]; col++) {
				float hgt = chart.getHeight(row, col);
				if (hgt > max) {
					max = hgt;
				} else if (hgt < min) {
					min = hgt;
				}
			}
		}
	}
	
	/**
	 * Oblicza wspórzędne środka wyświetlanego fragmentu mapy.
	 */
	public float[] getCenter() {
		float x = (viewArea[X0] + (viewArea[X1] - viewArea[X0]) / 2);
		float y = (viewArea[Y0] + (viewArea[Y1] - viewArea[Y0]) / 2);
		
		float[] vec = { x * chart.getDistance(), min + (max - min) / 2, y * chart.getDistance() };
		return vec;
	}
	
	/**
	 * Oblicza skale z która należy wyświetlać obiekty by cała mapa mieściła się
	 * na płutnie.
	 */
	private float autoscale(float chartSize, float distanceFromViewer, float zoom, float viewAngle) {
		return (float) ((2 * (double) zoom * (double) distanceFromViewer * Math.tan(Math.toRadians(viewAngle / 2))) / (double) chartSize);
	}
	
	/**
	 * Oblicza wielkosc wyswietlanych punktow kontrolnych.
	 */
	public float getNodeScale() {
		float result = 1.0f;
		
		if (scale < 1f) {
			result -= (1 - scale) * nodeThreshold;
		}
		
		return result * nodeRatio / scale;
	}

	/**
	 * Powiększa skalę.
	 */
	public void incScale() {
		scale += scale * scaleOffsetRatio;
	}

	/**
	 * Pomniejsza skalę.
	 */
	public void decScale() {
		scale = Math.max(0.0f, scale - scale * scaleOffsetRatio);
	}
	
	public void incViewAngle(int axis, float offset) {
		viewAngle[axis] += offset;
		if (viewAngle[axis] > 180) {
			viewAngle[axis] -= 360;
		} else if (viewAngle[axis] < 180) {
			viewAngle[axis] += 360;
		}
	}

	/**
	 * Przesuń mapę wzdłuż współrzędnej (X = 0, Y = 1, Z = 2).
	 */
	public void moveChart(int axis, float offset) {
		chartPosition[axis] += chart.getDistance() * moveOffsetRatio * scale * offset;
	}

	public synchronized int[] getSelection() {
		return selection;
	}

	public synchronized void select(int x0, int y0, int x1, int y1) {
		selection[X0] = Math.max(x0, viewArea[X0]);
		selection[Y0] = Math.max(y0, viewArea[Y0]);
		selection[X1] = Math.min(x1, viewArea[X1]);
		selection[Y1] = Math.min(y1, viewArea[Y1]);

		showSelected = true;
	}

	public void select(Area area) {
		int[] tmp = area.getArea();
		select(tmp[X0], tmp[Y0], tmp[X1], tmp[Y1]);
	}

	public synchronized void selectNone() {
		showSelected = false;
	}

	public synchronized boolean isSelected() {
		return showSelected;
	}

	// GETTERS AND SETTERS
	
	public contextType getType() {
		return this.type;
	}
	
	public float getFrustrumNear() {
		return this.frustrumNear;
	}

	public float getFrustrumFar() {
		return this.frustrumFar;
	}

	public float getFrustrumAngle() {
		return this.frustrumAngle;
	}

	public float[] getDefaultViewAngle() {
		return this.defaultViewAngle;
	}

	public float[] getDefaultChartPosition() {
		return this.defaultChartPosition;
	}

	public float[] getLightPosition() {
		return this.lightPosition;
	}

	public float[] getLightAmbient() {
		return this.lightAmbient;
	}

	public Chart getChart() {
		return this.chart;
	}

	public int[] getViewArea() {
		return this.viewArea;
	}

	public ControlPoints getControlPoints() {
		return this.controlPoints;
	}
	
	public void setControlPoints(ControlPoints controlPoints) {
		this.controlPoints = controlPoints;
	}

	public float[] getBgColor() {
		return this.bgColor;
	}

	public float[] getActiveGridColor() {
		return this.activeGridColor;
	}

	public float[] getInactiveGridColor() {
		return this.inactiveGridColor;
	}

	public float[] getActiveControlPointColor() {
		return this.activeControlPointColor;
	}

	public float[] getInactiveControlPointColor() {
		return this.inactiveControlPointColor;
	}

	public int getViewMargin() {
		return this.viewMargin;
	}

	public float[] getViewAngle() {
		return this.viewAngle;
	}

	public float getScale() {
		return this.scale;
	}

	public float getScaleOffsetRatio() {
		return this.scaleOffsetRatio;
	}

	public float getNodeRatio() {
		return this.nodeRatio;
	}

	public float getNodeThreshold() {
		return this.nodeThreshold;
	}

	public boolean isShowSelected() {
		return this.showSelected;
	}

	public float[] getChartPosition() {
		return this.chartPosition;
	}

	public float getMoveOffsetRatio() {
		return this.moveOffsetRatio;
	}		
}
